As promised in the previous part, B-Prep will start off simple, and get more complex as time goes on. The time for complexity has arrived.
File: BPREP-4.BAS
As first designed, the #if-#else-#endif preprocessor command set does not allow nesting. That is,
#if A
Some Code
#if B
More Code
#else
Different Code
#endif
#endif
is an illegal construct. If A is undefined, then the preprocessor will gobble lines until it hits "#else", at which point it will stop gobbling. If A is defined, then the preprocessor will work as expected. One possible way to solve this problem is to keep track of which #endif/#else goes with which #if:
#if A
Some Code
#if B
More Code
#else B
Different Code
#endif B
#endif A
This would enforce a better style. A lot of my time is spent looking at others' code, and trying to figure out which #endif goes with which #if. A better way is to run Analyze on the code blocks. As it stands, Analyze works on one line of code, and returns to its caller. The caller loops, reads in the next line of code, and calls Analyze. Rather than adding code for this to each command that needs it, we'll convert the caller into a subroutine, and call it whenever we get a block of code.
Go to the [Top] label, and remove it and the line "goto [Top]". Move the line "call GetNextLine ' Loads RawData$" to the start of Analyze. Now we need to add a loop to Analyze. You could use the "[Top]"/"goto [Top]", but I got a little fancier, and used "DO ... LOOP WHILE TRUE"
'---------------------------------------------------
sub Analyze
do
call GetNextLine ' Loads RawData$
if instr(RawData$,"#") <> 0 then
' Trim any leading spaces
PPCmdStr$ = trim$(RawData$)
' make sure the # is first in the line
if left$(PPCmdStr$,1) = "#" then
' Get the command
PPCmd$ = word$(PPCmdStr$,1)
' Trim it
PPCmdStr$ = trim$(mid$(PPCmdStr$,len(PPCmd$)+1))
select case PPCmd$
case "#define"
call DoDefine PPCmdStr$
case "#undefine"
call DoUndefine PPCmdStr$
case "#ifdef"
call DoIfDef PPCmdStr$
case "#else"
call DoElse
exit do ' Get out of the #IFDEF Analyze
case "#endif"
call DoEndIf
exit do ' Get out of the #IFDEF Analyze
' For Testing
case "#end"
exit do
end select
end if
else
call write RawData$
end if
loop while TRUE
end sub
Next, we'll need to adjust the "#if" command, and modify Analyze. "#endif" does nothing, and we want to keep it that way. "#else" gulps lines, and we want to keep that, too.
'--------------------------------------------------- sub DoIfDef TheName$ Du = IsDefined(TheName$) if Du <> 0 then ' It's defined call Analyze else ' Gulp until we find "#else" or "#endif" do call GetNextLine Du$ = word$(RawData$,1) if Du$ = "#else" then call Analyze exit do end if loop until Du$ = "#endif" end if end sub
Add the code, and test it out. You should now be able to #define a variable within a #if block, something you could not do previously.
File: BPREP-5.BAS
The next command will be "#include". To start this, we'll need to modify the initialization subroutine as well as the input and output subroutines and the error handler. We'll also need a test program or two. We'll start with the testers:
' File Test5-1.bas FOR n = 1 TO 10 PRINT "Who are you?" NEXT n
There's no #include command yet -- we're just seeing how well we can read & write a single file.
' File: Test5-2.bas PRINT "In Test1.mod"
Now, we'll change InitializeSystem by adding an OPEN command. File handles are naturally Global, so that saves a little work. We won't do any writes to a file yet -- we want to see where we're going.
'---------------------------------------------------
sub InitializeSystem
FILEDIALOG "File to Process", "*.*", FilePath$
InHandle$ = "1"
open FilePath$ for input as #InHandle$
' Get the File Path
for n = len(FilePath$) to 1 step -1
Du$ = mid$(FilePath$,n,1)
if Du$ = "\" then
Path$ = left$(FilePath$,n)
exit for
end if
next n
call write "' BASIC Preprocessor"
call write "'"
end sub
'---------------------------------------------------
sub DoError ErrMsg$
call write ErrMsg$
close #InHandle$
input A$
end
end sub
'+++
We'll have to modify GetNextLine to read from files. So far, we're just opening and closing one file, so we don't need to get too fancy; however, note that I'm changing GetNextLine from a SUB to a FUNCTION, to flag when we've hit the end of the file.
'---------------------------------------------------
function GetNextLine()
' Eats up blank lines
do
[CheckEOF]
if eof(#InHandle$) <> 0 then
if FHAIndex > 0 then
close #InHandle$
InHandle$ = FileHandleArray$(FHAIndex)
FHAIndex = FHAIndex - 1
goto [CheckEOF]
else ' FHAIndex
close #InHandle$
RawData$ = ""
GetNextLine = FALSE
exit do
end if ' FHAIndex
end if ' eof
line input #InHandle$, RawData$
Du = instr(RawData$, COMMENT$)
if Du <> 0 Then
RawData$ = left$(RawData$,Du-1)
end if
RawData$ = RTrim$(RawData$)
GetNextLine = TRUE
loop while RawData$ = ""
end function
Run the code. You should see the File Dialog come up. Select "TEST.BAS", and you'll see the contents of TEST1.BAS getting printed on the console. Now, we'll need to add the "include" command to Analyze, and a subroutine for it, along with a function to test if the file exists.
'--------------------------------------------------- sub DoInclude TheFileName$ Du = instr(TheFileName$,chr$(34)) ' Test for quotes if Du = 0 then call DoError "Include filenames must have "+chr$(34)+"s" end if File$ = mid$(TheFileName$,Du+1) Du = instr(File$,chr$(34)) ' Test for quotes if Du = 0 then call DoError "Include filenames must have "+chr$(34)+"s" end if File$ = left$(File$,Du-1) if File$ = "" then call DoError "Include must have a filename." else ' Load InHandle$ into next entry of array FHAIndex = FHAIndex + 1 if FHAIndex > MAXFILES then call DoError "Maximum number of open files exceeded." end if FileHandleArray$(FHAIndex) = InHandle$ ' Iniz file handle InHandle$ = "1" ' Add current path if none is shown if instr(File$,"/") = 0 then FilePath$ = Path$+File$ end if ' Open the file if FileExists(File$) then open FilePath$ for input as #InHandle$ else call DoError File$+" does not exist in "+Path$ end if end if end sub '--------------------------------------------------- function FileExists(File$) files Path$, File$, FileInfo$() FileExists = val(FileInfo$(0, 0)) 'non zero is true end function
Note that there is a lot of testing going on in the function. I'd rather have the software tell me why it failed rather than the OS error message, which is usually less than helpful! Also, I've limited the maximum number of files that can be open simultaneously to 10, which should be more than enough. If not, just change MAXFILES.
File: BPREP-6.BAS
We'll wind this up with modifying the way the outputs are written. Up to this point, all output has gone to the main window. This is OK for testing, but for serious work we need to put the preprocessor output to a file, and use some popup windows for the error messages.
First, we'll turn off the main window, and add a global name for the output handle. Call it "OutHandle"!
NOMAINWIN '====== GLOBALS ======= global TRUE, FALSE, TRUE$, FALSE$ global COMMENT$, TAB$ FALSE = 0 TRUE = -1 FALSE$ = "0" TRUE$ = "-1" COMMENT$ = "'" TAB$ = CHR$(9) global RawData$ ' Definitions global MAXDEFS, Definition$, NextDef MAXDEFS = 1000 DIM Definition$(MAXDEFS) NextDef = 1 ' Include ' Most files open at one time global MAXFILES ' Working file handle global InHandle$ ' Fully-qualified file name global FilePath$ ' Path (from FilePath$) global Path$ MAXFILES = 10 ' Holds open handles DIM FileHandleArray$(MAXFILES) ' Points to current FileHandleArray$ entry global FHAIndex FHAIndex = 0 ' Used to see if file exists dim FileInfo$(10, 10)
Then we'll need to add a FILEDIALOG to InitializeSystem to get the output file name, and close the handle in GetNextLine().
'---------------------------------------------------
sub InitializeSystem
FILEDIALOG "File to Process", "*.*", FilePath$
InHandle$ = "1"
open FilePath$ for input as #InHandle$
' Get the File Path
for n = len(FilePath$) to 1 step -1
Du$ = mid$(FilePath$,n,1)
if Du$ = "\" then
Path$ = left$(FilePath$,n)
exit for
end if
next n
FILEDIALOG "File to Write to", "*.*", OFilePath$
open OFilePath$ for output as #OutHandle
call write "' BASIC Preprocessor"
call write "'"
end sub
'---------------------------------------------------
function GetNextLine()
' Eats up blank lines
do
[CheckEOF]
if eof(#InHandle$) <> 0 then
if FHAIndex > 0 then
close #InHandle$
InHandle$ = FileHandleArray$(FHAIndex)
FHAIndex = FHAIndex - 1
goto [CheckEOF]
else ' FHAIndex
close #InHandle$
close #OutHandle
RawData$ = ""
GetNextLine = FALSE
exit do
end if ' FHAIndex
end if ' eof
line input #InHandle$, RawData$
Du = instr(RawData$, COMMENT$)
if Du <> 0 Then
RawData$ = left$(RawData$,Du-1)
end if
RawData$ = RTrim$(RawData$)
GetNextLine = TRUE
loop while RawData$ = ""
end function
"write" will need to be changed to send output to the output file handle:
'---------------------------------------------------
sub write aString$
print #OutHandle, aString$
end sub
'---------------------------------------------------
sub Analyze
if GetNextLine() = TRUE then ' Loads RawData$
do
if instr(RawData$,"#") <> 0 then
' Trim any leading spaces
PPCmdStr$ = trim$(RawData$)
' make sure the # is first in the line
if left$(PPCmdStr$,1) = "#" then
' Get the command
PPCmd$ = word$(PPCmdStr$,1)
' Trim it
PPCmdStr$ = trim$(mid$(PPCmdStr$,len(PPCmd$)+1))
select case PPCmd$
case "#define"
call DoDefine PPCmdStr$
case "#undefine"
call DoUndefine PPCmdStr$
case "#ifdef"
call DoIfDef PPCmdStr$
case "#else"
call DoElse
exit do ' Get out of the #IFDEF Analyze
case "#endif"
call DoEndIf
exit do ' Get out of the #IFDEF Analyze
case "#include"
call DoInclude PPCmdStr$
end select
end if
else
call write RawData$
end if
loop while GetNextLine() = TRUE
end if
end sub
Finally, we'll send the error messages to a notice window, and add a completion notice to the end.
'--------------------------------------------------- sub DoError ErrMsg$ notice "Fatal Error!" + chr$(13) + ErrMsg$ close #InHandle$ close #OutHandle end end sub '+++ notice "Preprocessing is Complete." end
Once this is done, we can try running tests. The test files are Test6-1.bas through Test6-4.bas. Double-click on Test6-1.bas to select it for input, then enter a filename for the results (output) file. I use "Du.bas" -- a carry-over from math class when Du was a dummy variable.
Once the preprocessing is finished, check Du.bas to make sure that what you've expected to happen has indeed happened.
Enjoy!
Files
The file PreProcess.zip is included in the zipped archive of this newsletter.